import sqlite3, simplejson, random, time

from twisted.application    import internet, service
from twisted.internet       import protocol, reactor, defer, ssl
from twisted.protocols      import basic
from twisted.python         import log
from push                   import APNSNotification
from OpenSSL                import SSL



class Provider(service.Service):
    def __init__(self, databaseFile,flushinterval):
        log.msg("Starting Provider")
        self.db = Database(databaseFile)
        self.__pushfactory = []
        self.__devices = self.initDevicesFromDatabase()
        self.__storeDevices(interval=flushinterval)
       
        reactor.addSystemEventTrigger('before', 'shutdown', self.__storeDevices)
        
        # ==========================================================================
        # = Testcode =
        # ==========================================================================
        customPayload = ["my","name","is"]
        try:
            # Thorsten -  8bac234d057ac9a8d009f1746b00ac6a220089709be3e995cc61b3a253fbbd3e 
            # Timo     -  5f20370fe3d5bee7e2a6d802f77eccdadcc7c945f9204424d345e371dfb8ab68
            device = list(self.getDevices(deviceToken="8bac234d057ac9a8d009f1746b00ac6a220089709be3e995cc61b3a253fbbd"))[0]
            #print self.__devices.keys()[0]
            #print self.getDevices(token=self.__devices.keys()[0])
            #device = list(self.getDevices(token=self.__devices.keys()[0]))[0] # The first Token in the database
            reactor.callLater(5,device.sendAlert,"pyshD started.",hasActionKey=False, playSound=True)
            #reactor.callLater(5,self.deleteDevice,"8bac234d057ac9a8d009f1746b00ac6a220089709be3e995cc61b3a253fbbd3e")
            # notification = Notification(alert="Test",badge=2)
            # reactor.callLater(5.5,self.sendMessage,self.devices.keys()[0],notification)
            # notification = Notification(alert="Test2",badge=3)
            # reactor.callLater(9,self.sendMessage,self.devices.keys()[0],notification)
            #reactor.callLater(5.5,self.sendMessage,self.devices.keys()[0],notification)
        except NoSuchDevice:
            pass
        # ==========================================================================
        
        log.msg("pyshD Provider started.")
    
    def getDevices(self,deviceToken=None):
        """Yield a list of deviceObjects that match the criterias provided.
        Of course a deviceToken is unique"""
        if deviceToken is not None:
            try:
                device = self.__devices[deviceToken]
            except KeyError:
                raise NoSuchDevice("Invalid token.")
            yield device
        else:
            for device in self.__devices.values():
                yield device
    def getDeviceWithDevicetoken(self,deviceToken):
        """Return the deviceobject or raise a NoSuchDevice"""
        return list(self.getDevices(deviceToken=deviceToken))[0]
    
    def sendMessage(self,devicetoken,notificationObject):
        """Don't use this method directly. Instead use the corresponding method of a Device Object"""
        payload = notificationObject.asJSON()
        try:
            pushfactory = random.choice(self.__pushfactory)
        except IndexError:
            log.err("No Connectors available. Dropping Message")
            return
        pushfactory.sendMessage(devicetoken,payload)
    
    def updateDevice(self,deviceToken,badgeSetting=False,soundSetting=False,alertSetting=False):
        """Register or update new device.
        deviceToken: A Hex representation of Devicetoken.
        badgeSetting: True/False
        soundSetting: True/False or a filename.
        alertSetting: True/False or a customsetting(str)"""
        log.msg("[%s] - Updating Device" % deviceToken)
        try:
            self.__devices[deviceToken].setBadgeSetting(badgeSetting)
            self.__devices[deviceToken].setSoundSetting(soundSetting)
            self.__devices[deviceToken].setAlertSetting(alertSetting)
        except KeyError:
            self.__devices[deviceToken] = Device(self,deviceToken,badgeSetting=badgeSetting,soundSetting=soundSetting,alertSetting=alertSetting)
    
    def deleteDevice(self,deviceToken,timestamp=None):
        """Delete a device from the Provider
        deviceToken: well.. the DeviceToken
        timestamp: if specified the timestamp is compared to the lastModified value in the database.
                   if the last_modified time is before (younger) than the specified timestamp no action is taken.
        """
        try:
            device = self.__devices[deviceToken]
        except KeyError:
            return # Not in the provider
            
        lastModified = device.lastModified()
        if timestamp is None:
            timestamp = lastModified + 1
            
        if timestamp > lastModified:
            log.msg("[%s] - Deleting" % deviceToken)
            del(self.__devices[deviceToken])
            c = self.db.connection.cursor()
            c.execute("DELETE FROM device WHERE deviceToken=?",(deviceToken,))
            self.db.connection.commit()
        
    
    def initDevicesFromDatabase(self):
        """Returns a dictionary with Device Objects. Key: Devicetoken Value: Device Object"""
        devices = {}
        c = self.db.connection.cursor()
        c.execute("SELECT deviceToken, badgeSetting, soundSetting, alertSetting, badgeCount, lastModified FROM device")
        for row in c:
            devices[row[0]] = Device(self,row[0],badgeSetting=row[1],soundSetting=row[2],alertSetting=row[3],badgeCount=int(row[4]),lastModified=int(row[5]),stored=True)
        return devices
    
    def registerPushfactory(self,factory):
        """Register a push Client Factory with the Provider. This method is invoked by the ClientFactory"""
        self.__pushfactory.append(factory)
    
    def __storeDevices(self,interval=30.0):
        """Flush Device Object to the database.
        interval: Every n seconds"""
        log.msg("Flushing Devices to the database")
        interval=interval
        for device in self.__devices.values():
            device.store()
        
        # Call us again in $interval seconds
        self.call = reactor.callLater(interval,self.__storeDevices,interval=interval)
    



class Device(object):
    """A Device is an iPhone or iPod Touch. Devicetoken, Notificationssettings and such.."""
    def __init__(   self,provider, 
                    deviceToken=None, 
                    badgeCount=0, 
                    badgeSetting=False, 
                    soundSetting=False, 
                    alertSetting=False,
                    stored=False,
                    lastModified = int(time.time()),
                ):
        super(Device, self).__init__()
        self.__provider = provider
        
        self.__deviceToken = ""
        self.__badgeCount = int(0)
        self.__badgeSetting = badgeSetting
        self.__soundSetting = soundSetting
        self.__alertSetting = alertSetting
        self.__lastModified = lastModified
        self.__failcount = 0
        
        self.setDeviceToken(deviceToken)
        self.badgeSetTo(badgeCount)
        self.setBadgeSetting(badgeSetting)
        self.setSoundSetting(soundSetting)
        self.setAlertSetting(alertSetting)
        
        # Race condition... but not bad. I guess ;-)
        # Only thing that could happen is that some devices get written to the 
        # database (provider.__store starts while setting Values.)
        # even if they're not changed...
        self.__stored = stored
    
    def getDeviceToken(self):
        return self.__deviceToken
    
    def getBadgeCount(self):
        return self.__badgeCount
    
    def getBadgeSetting(self):
        return self.__badgeSetting
    
    def getSoundSetting(self):
        return self.__soundSetting
    
    def getAlertSetting(self):
        return self.__alertSetting
    
    def getLastModified(self):
        return self.__lastModified
    
    def getFailCount(self):
        return self.__failcount

    def lastModified(self):
        """return the modification time"""
        return self.__lastModified
    
    def sendFailed(self):
        """Increase the failcount for this device"""
        self.__failcount += 1
        if self.__failcount > 10:
            log.err("[%s] - Failed more than 10 times in a row. Goodbye!" % self.__deviceToken)
            self.__provider.deleteDevice(self.__deviceToken)
            self.failcall.cancel()
        self.failcall = reactor.callLater(60,self.failReset) # if no fails for 1 minute reset the failcount
    
    def failReset(self):
        self.__failcount = 0
    
    # =================
    # = Sending stuff =
    # =================
    def sendPayload(self,payload):
        """Send the payload (Notfication Object)"""
        if payload.badge is None:
            payload.badge = self.__badgeCount
        self.__provider.sendMessage(self.__deviceToken,payload)
    
    def sendAlert(self,body,isLocalizable=False,incrementBadge=True,playSound=True,hasActionKey=False,alertType="Default",*locargs,**custom):
        """Send an Alert to the Device. ()
        Compare: http://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW20
        
        body (String)           The Message Text
        loc-args                All other non keyword arguments (if there are any) are treated as message arguments (loc-key assumed)
        isLocalizable (Bool)    Default is True. If True the body is treated as loc-key.
        incrementBadge (Bool)   Default is True. If True the Badgecount is incremented
        playSound (Bool)        If True the Sound from self.__soundSetting is played.
        hasActionKey (String)   Default is False meaning only an "okay" button is presented. True Means Default button. Alernativly you may specify a custom locaizable Text.
        alertType(String)       Devices can registert for All ("Default") Alerts or for a custom type. If the value is "Forced" the self.__AlertSetting is ignored.
        
        All other keyword arguments are appended as custom payload.
        """
        
        payload = APNSNotification()
        
        if isLocalizable:
            payload.localized = body
        else:
            payload.alertbody = body
        
        payload.button = hasActionKey
        
        if len(locargs) > 0:
            payload.localizedArguments = locargs
        
        if incrementBadge:
            self.__badgeCount = self.__badgeCount + 1
            self.__stored = False
            payload.badge = self.__badgeCount
        if self.__soundSetting is not False:
            payload.sound = playSound
        payload.customJSON = custom
        
        if alertType == "Default":
            alertType = True
        
        if(alertType == "Forced" or alertType == self.__alertSetting):
            self.sendPayload(payload)
    
        
    def badgeIncrement(self):
        """Increment the Badgecount by 1"""
        self.badgeSetTo(self.__badgeCount + 1)
    
    def badgeDecrement(self):
        """Decrement the Badgecount by 1"""
        self.badgeSetTo(self.__badgeCount - 1)
    
    def badgeSetTo(self,badgeCount):
        """Set self.__badgeSetting"""
        try:
            if badgeCount < 0:
                badgeCount = 0
        except:
            pass
        self.validateBadgeCount(badgeCount)
        self.__badgeCount = badgeCount
        self.__stored = False
        payload = APNSNotification(badge=self.__badgeCount)
        self.sendPayload(payload)
        
    
    def setDeviceToken(self,deviceToken):
        """Set self.__deviceToken"""
        self.validateDeviceToken(deviceToken)
        self.__deviceToken = deviceToken
        self.__stored = False
    
    def setBadgeSetting(self,badgeSetting):
        """Set self.__badgeSetting"""
        self.validateBadgeSetting(badgeSetting)
        self.__badgeSetting = bool(badgeSetting)
        self.__stored = False
    
    def setSoundSetting(self,soundSetting):
        """Set self.__soundSetting"""
        self.validateSoundSetting(soundSetting)
        if soundSetting == 1 or soundSetting == "1":
            soundSetting = True
        if soundSetting == 0 or soundSetting == "0":
            soundSetting = False
        self.__soundSetting = soundSetting
        self.__stored = False
    
    def setAlertSetting(self,alertSetting):
        """set self.__alertSetting"""
        self.validateAlertSetting(alertSetting)
        if alertSetting == 1 or alertSetting == "1":
            alertSetting = True
        if alertSetting == 0 or alertSetting == "0":
            alertSetting = False
        self.__alertSetting = alertSetting
        self.__stored = False
    
    def store(self):
        """docstring for store"""
        if self.__stored:
            return
        else:
            c = self.__provider.db.connection.cursor()
            c.execute("SELECT id FROM device WHERE deviceToken=?", (str(self.__deviceToken),))
            if list(c):
                log.msg("[%s] - Updating database." % self.__deviceToken)
                c.execute("UPDATE device set badgeSetting=?, soundSetting=?, alertSetting=?, badgeCount=?, lastModified=? WHERE deviceToken=?",
                            (   self.__badgeSetting,
                                self.__soundSetting,
                                self.__alertSetting,
                                self.__badgeCount,
                                self.__lastModified,
                                self.__deviceToken
                                )   )
                self.__provider.db.connection.commit()
            else:
                log.msg("[%s] - Creating in database." % self.__deviceToken)
                c.execute("INSERT INTO device (deviceToken,badgeSetting,soundSetting,alertSetting,badgeCount,lastModified) VALUES(?,?,?,?,?,?)",
                            (   self.__deviceToken,
                                self.__badgeSetting,
                                self.__soundSetting,
                                self.__alertSetting,
                                self.__badgeCount,
                                self.__lastModified
                                   )   )
                self.__provider.db.connection.commit()
            
            self.__stored = True
        
    
    # ======================
    # = Validation Methods =
    # ======================    
    def validateDeviceToken(self,deviceToken):
        """Checks if the deviceToken is in the correct format. 32byte long, Hex encoded"""
        #29d445f72b4b26431efb90c673f751af64ed1745fdba4b832e1f244651aacca0
        try:
            try:
                bintoken = deviceToken.decode("hex")
            except TypeError:
                log.err()
                raise InvalidDeviceToken(deviceToken)
            if len(bintoken) is not 32:
                raise InvalidDeviceToken(deviceToken)
        except TypeError:
            log.err()
            raise InvalidDeviceToken(deviceToken)
        except Exception:
            log.err()
            raise
        
    
    def validateBadgeCount(self,badgeCount):
        """Validates the badgeSettings or raises InvalidBadgeSetting Exception.
        Boolean Value"""
        try:
            badgeCount = int(badgeCount)    
        except ValueError:
            log.err()
            raise InvalidBadgeCount(badgeCount)
        if badgeCount < 0:
            raise InvalidBadgeCount(badgeCount)
        
    
    def validateBadgeSetting(self,badgeSetting):
        """Validates the badgeSettings or raises InvalidBadgeSetting Exception.
        Boolean Value"""
        try:
            badgeSetting = bool(badgeSetting)    
        except ValueError:
            log.err()
            raise InvalidBadgeSetting(badgeSetting)
        
    
    def validateSoundSetting(self,soundSetting):
        """Validated the soundSettings value or raises InvalidSoundSetting.
        Maximum length is 16 characters (payload size is limited. So keep the names short)"""
        try:
            soundSettings = str(soundSetting)
        except Exception, e:
            log.err()
            raise InvalidSoundSetting(soundSetting)
        
    
    def validateAlertSetting(self,alertSetting):
        """Validates  the provided alertSetting or raises InvalidAlertSetting.
        Usually this setting is Boolean. But you may create as many custom settings as you wish.
        You may later filter on those settings.
        Limited to 32 characters. (just to limit something)"""
        try:
            alertSetting = str(alertSetting)
        except Exception, e:
            log.err()
            raise InvalidAlertSetting(alertSetting)
        
        if len(alertSetting) > 32:
            raise InvalidAlertSetting(alertSetting)
        
    



class Database(object):
    """Init and Acces the Database (storing Devicetokens and stuff ...)"""
    def __init__(self, filename):
        super(Database, self).__init__()
        self.filename = filename
        self.connection = sqlite3.connect(self.filename)
        
        #Check if Schema is there. Otherwise init.
        
        c = self.connection.cursor()
        try:
            c.execute("SELECT COUNT(*) FROM device")
            log.msg("%s devices registered" % list(c)[0])
        except sqlite3.OperationalError:
            log.msg("Schema empty")
            self.initSchema()
            log.msg("Created a new empty schema")
            
    
    def initSchema(self):
        c = self.connection.cursor()
        sql = "CREATE TABLE 'device'"
        sql += "("
        sql += "id INTEGER PRIMARY KEY AUTOINCREMENT,"
        sql += "devicetoken VARCHAR UNIQUE,"
        sql += "badgeSetting INTEGER,"
        sql += "soundSetting VARCHAR,"
        sql += "alertSetting VARCHAR,"
        sql += "badgeCount VARCHAR,"
        sql += "created_on DEFAULT CURRENT_TIMESTAMP, "
        sql += "lastModified NOT NULL"
        sql += ")"
        sql += ""
        c.execute(sql)


# ==============
# = Exceptions =
# ==============

class InvalidDeviceToken(Exception):
    """This Exception is raised when the Devicetoken isn't valid"""
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)
    

class InvalidBadgeSetting(Exception):
    """This Exception is raised when the BadgeSetting isn't valid"""
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)
    

class InvalidSoundSetting(Exception):
    """This Exception is raised when the Soundfile isn't valid"""
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)
    

class InvalidAlertSetting(Exception):
    """This Exception is raised when the alert setting isn't valid"""
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)
    

class NoSuchDevice(Exception):
    """Raised if the specified device isn't registered"""
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)
    



class SSLServerContext(ssl.DefaultOpenSSLContextFactory):
    """docstring for SSLServerContext"""
    def __init__(self, certificate, privatekey, CAfile):
        self.ctx = SSL.Context(SSL.SSLv3_METHOD)
        self.ctx.use_certificate_file(certificate)
        self.ctx.use_privatekey_file(privatekey)
        self.ctx.set_verify     (
                                    SSL.VERIFY_PEER |  # If you (client) have a cert... give it to me
                                    SSL.VERIFY_FAIL_IF_NO_PEER_CERT | # If you don't have one.. i fail
                                    SSL.VERIFY_CLIENT_ONCE, # Not sure what this does. Best description found is: Do not request client certificate on renegotiation.
                                    self.verify_callback
                                )
        self.ctx.load_verify_locations(CAfile)
    
    def getContext(self):
        return self.ctx
    

        
    def verify_callback(self,connection, x509, errnum, errdepth, ok):
        # x509 'add_extensions', 'digest', 'get_issuer', 'get_pubkey', 'get_serial_number', 
        # 'get_subject', 'get_version', 'gmtime_adj_notAfter', 'gmtime_adj_notBefore', 'has_expired', 
        # 'set_issuer', 'set_pubkey', 'set_serial_number', 'set_subject', 'set_version', 'sign', 'subject_name_hash'
        if not ok:
            peername = connection.getpeername()
            subject = x509.get_subject()
            print("%s User %s not allowed.") % (peername,subject.commonName)
            log.err("Bad Cert. Issuer: %s Subject: %s Pubkey: %s Serial: %s Version: %s Expired: %s" 
                    % (     x509.get_issuer(),
                            x509.get_subject(),
                            x509.get_pubkey(),
                            x509.get_serial_number(),
                            x509.get_version(),
                            x509.has_expired()
                    ))
            return False
        else:
            peername = connection.getpeername()
            subject = x509.get_subject()
            print("%s User %s authenticated.") % (peername,subject.commonName)
            print "Certs okay."
        return True


if __name__ == "__main__":

    pass